#!/usr/bin/python # -*- coding: utf-8 -*- """ Skeleton Key """ # Copyright (c) 2020 University of Utah Student Computing Labs. ################ # All Rights Reserved. # # Permission to use, copy, modify, and distribute this software and # its documentation for any purpose and without fee is hereby granted, # provided that the above copyright notice appears in all copies and # that both that copyright notice and this permission notice appear # in supporting documentation, and that the name of The University # of Utah not be used in advertising or publicity pertaining to # distribution of the software without specific, written prior # permission. This software is supplied as is without expressed or # implied warranties of any kind. ################################################################################ # skeleton_key.py ################################################# # # A Python Tk application to set/unset the firmware password. # # # 0.1.0 2017.04.13 Initial build. tjm # 0.2.0 2017.05.04 single pane. tjm # 1.0.0 2020.01.22 Initial release, # JAMF and Slack integration, # hash generation, reading config file. tjm # ################################################################################ # notes: ####################################################################### # # sudo /usr/local/bin/pyinstaller --onefile Skeleton_Key.spec # # # # ################################################################################ from __future__ import division from __future__ import print_function import base64 import ConfigParser import hashlib import inspect import json import os import platform import plistlib import pwd import re import socket import subprocess import sys import tkFileDialog import tkSimpleDialog import ttk from Tkinter import Tk, N, E, S, W, StringVar, IntVar, PhotoImage, HORIZONTAL import logging import pexpect import requests try: import mount_shares_better as msb except: pass class SinglePane(object): """ Load keys, generate hashes, toggle fwpw """ def __init__(self, root, logger, admin_password, fwpw_status, master_version): """ Initialize object and variables """ self.logger = logger self.logger.info("%s: activated" % inspect.stack()[0][3]) self.root = root self.root.title("Skeleton Key " + master_version) self.logo = '''\ R0lGODlhWAJRAPcAAAEAAAgHBwwLCxAPDxQTExgXFxwcHCEfHyYlJSgnJywsLD0nJzItLTY2Njg3 Nzw8PF4dHGMcG24cGnIdG1QgH18gH0EmJkgpKEA/P1glJEJCQkhHR0tLS1BPT1NSUlhXV1lZWWFf X3NcXGRjY2hnZ2xra3Bvb3ppaXRzc3h2d318fJcAAJsAAp8AC4obGJ4dGKAADaEAFKQAGqceGbAf GqUhHqwhHrQhHL4hG6cEIqgEI6cKI6kNJqkOKKkQJ6sWK6sYLKwbMJgsKpw0MpI0M6UlIqEoJaEs KqolIqMxL68lNLEpOaE1M6E5N6M+PbMwPp9AP7QyQrc8SZ1DQpxLSplHRp5ZWKJDQqFLSrpBTaFQ T79OWKVZWIBuboB+fop4eKxraqdnZql6ebN+fbJ2dcNXX8JXYcRcZsZeacdjbshmb8hmcM92freA f896gnKBgISEhIyMjIiGh5OTk5ycnJiWlpCJibuMi6Cfn76enqyJiaSjo6inp6qqqrCvr7+trbS0 tLi3t7m5ubm3uNODiteOltiPlsSZmNeRltiRltqUmtqYncCMi+KboMSkpMqqqt+mqt6ho8C/v8q6 uti7u+Cnq+GqruuiqeOusuSxs+K5vMTExMjHx8zMzN/GxtDPz9/Jyc/Q0NLS0tjX19zc3O3KzOjH yO/P0eTV1e/R0+Pa2u/e3vDR0/Lc3fTe4N/h4OTk5Orl5erq6unn6PPt7vjt7vT09Pr29/////f4 +Pnu8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAABYAlEA AAj+AHEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKFAlLkBwQGho00ABCjiBY I2PKnEmzps2bOHPq3MmzJ8FZdRoEAEAAgYKjChAQABCAwRyYPqNKnUq1qtWrWLPmhFUiQIADChgg HYsUgQAAIUhpXcu2rdu3cOPKVfgGwICwYsnqRbo0xdy/gAMLHky4sEBYBgDsXbyXqFrDkCNLnky5 ckRAAAzkZcw57AEAfSyLHk26tOmrfQAY7cw6LAIAeE7Lnk27tm2JggK03o0U9O3fwIMLn/xKNe/j AEQNX868ufOqAgwcP24gwPPr2LNr50hC8XTeDAL+dNhOvrz587hgAdjMmP10BgAeo59Pvz5tDwTc L1YKQICAAAOsxpoBGthn4IEIQqaefnsFYMAbm5BCiiRyVCcgZ/ElqOGGHK6VggCdMVAdHrkYlEsf A2jWGQEkdOjiizDmZMCFeok4wCsKvUKAipwRUGKMQAYpZEakeIchjgvJ8lVnAIwy5JNQRqlQH7q1 F4AfDklSJWMB0CHllz0BICaYo6FAAGcGNACRBtIxRsAIZMZ5k5gAyFkZm5wFAAhEgqzHWJp2BhoT nYJKpgBYXELlkJKcIYDAj4UWJkhowREaaWG5zNijLRDZoul+BtACGB0lkFBqCaiqRUcKkMaF6qv+ cWziap0GCYKqrAXNgqqXkFl66WCZ0jjWAQZwClGb+/kIWAkAvPqqWmJ28pdqr4oZB1zMHpQaACAY RIeYJUTm66+BeSrsWAQY+9CniwnQ6qwIbULpXM0OlAsIAODaVrYGpfaafAJp8Fq4vY5JrmDBoqlu Q+b2+C62tIpLsECj+OYWvwWl9i2vAm0CW70FR3zwX5kiuhexCzNkS1GMEfvwWxgXNOlAfQgySxzh jtLHLLam4KQoKZRAx499zDtLzQN10geSguCcgiAE1XwzwSfuKgtCIHfs29FWEwRI0HQoKogKQs+C i7yKFj3Q0foKEnQcTg6ksyy2Qo2L2yQMHTP+zU06gABBKQAwS9ay9LGr2RTvIUsgJYwwLy4npkAC 3AWNgnMcpMw9ECykltAHpHMDUoLdEI37UCqQYKK66pms7jomlqwiEC2WWIJJJq2/7nolpozsUMJu Xv1QLmcyhoABL1+cnCijdCLKYxgDIDAI4aZWAgLMgsatBtwKFIfgAvk70PdmB67B9QBwLD0C1EPu AQAeMIsAwANlvclrsoiiQLPvIyD8t+d7jZO+5QD0jaJPditOhu4GALt9TwHou5ZA9gA/7EENXwFE APe01aTU2C0XAPALyEQxMHwhAHGpAQH2XsOrXHCPemLS17eahS9mKQcXpCgh/H5EQRWWYE/+ETGd Q9ywghjIwIhIlIESY8DEGLAgEgIxRQxacMQkKnGJR2RBGXznEHbtJV2dQtZejpe8fdGJTgSLnsVw kZoPlMhjDRRI4PbkMUqpsEkCYZ9ARIGrXDBLeGKaVwkUcENZaKBABjmjmBAAxM9pTYLwE8iJStQ9 yO0hFyD0Cy7o8BpKeQEAJUqNBHHxPUqlRgNEA4AEc/E9kfFtFOohWGpklTVHnk2V4ZNeiVwIPlwA 4jGy0CDFdrjH10grFwoAAeI6gUs26pIiQmwIIWLgAyBUEwjYxOY1gbADGVhCIKXYgQ588IMfWDOb 58QmDMzARYbk4gAm04tSUraQlcWTLGT+XFaTOtG8UUAvYtIjCB/iKJCAao1SCPDL4AChgDrgomKP wwU/v3dDg+JicD4TRShE8T3hEUR6RevDJt4lC36CTGBxGwhKC5KCv+FCA3FIwQcKqkmBQSoXwnQm EF/6KHu9hoPKId9LEZm1w5hUlgAIxEC+lVLI8fN93ltgLp3Upz7wUxSiQEkuSSeRaDJkmugM6zaB EANIgNMHOwBCOcMa1hiws50LEeMXPcqw4u3nAGVky94GEr2JOTOlWYNoVE8EyjgU6FuI28T7zhi3 rDFTkdFKpF/tFYfXoFEgpOBeQqWFQ82m4IZ9ilC+MDMLj9mtqLjozlTrN9m9OlM5FaP+Q5EoBTJW WhZcueSsMznbBwwoUiB7EwUApEVByNIqNU0NosEiAla2stUH3gTnDtLqXG1iMwZogOtC4NkyAtBV ZXYdI/JsQgo/2KELJ/gCHkZKEdfioq9RGy5rByJcSnlsEynolsdIAYJuPZRbuEoNZwOrSucxj58P Qy1wQQNIv/KMeyl9cHIgl77UYBI0dAjAjxDggYJ4QAG5DEX9/DsQqPZLvkMVKi5AxqydFRSpKUWu M1PwTzn2UiB9clJq+oDVfvIWxRPx6kKa24PqXlMGZsVFKXwwzuqi063aVcgBzsWX79bTAPccSz5n ggowMMEGN8CBmGkgBCw4YiLuhe/+K+c7zHkpIHCUerPFZLzgxvpVg5AixcsUvOKJVYxg8qkvDpNm sRSw5A1yRAmio3rD/0qQzrgIXEor5srVOjOEbM6acJGq29QoB2M4pRVmNAk57kmrSKR+KN9029Xl QqS56Yx1NpF8Vuqis8hqDYI5sXsQWCDOXr6WJCx+BAtbvIJEXAFELuAwyoZ04ofA/nV6ZJFXg9AC D/RzyCwUhUMvjQLaUXlnlsciAHoqxBYgMp4BZvIIJoj53fB+9w3AEJE0A9SvAmazRNfYSsT1G8fw 42fgUFzUPoEAEDpL6EH4zD0eA2JguCBhCTYhCmYhHHub+DZBMbNxMZFuFo7icR/+HIVCIBcHAYIY xR4cVendSjKGbG64KB4OMk+vmpSqHMUm8BWx92mgD5wUYFRTkPFSWjrIrn4IrJ0MhB7QWslMdnI5 ywnlggBCACQWCGbGg4v3XasTAhjB9/bATBCohwC3eMj34CSQOfRnDwKhg3/g/pBvZZ0guWAvDlnN AQHodgQA4AQF2R6VcaMr2wohhVzxOV6R0MLd8Y48vG9wh4fYW9+4oCBgJyZorZHYY1mfYbNm+WKr 37YEe54sZrmnGkFwOHy35dXI6cQxEILy5bcfSGbRKB9In431CDBcy23uPUSWvrOLfLh/if9XyGUv fYH70Ym49/N8x/2MHPO9clv+/tUYMB2d0VXydNmKa7Ve961eAwAHClIcAggkMR3GQwhpIQeeNWsW d6k2QShI6oGznQNiMgcQcTOsRhANoCybEACp5lv60glwkHmYJhXcFTydEl7ytG4i4QiSt4HxNgVx AQv+9BDMY2UOgWAHMQqi8C6i0An6hxCkIAokeBCkwIIcwTwtSBAvKG0I8S0AYwvNc4MOIWQK4QYs YERO1k1OBEW4IEUtQE3V9QM6EAMrsEVWF0kF4QCCQwoEIAAOgAsqkC+kQGMeUwL4pyaK5QAgIC2d QAKjkALXAgjnEzikNgLS8Te0IAAHeC17wAEOMAIwAQgjQApCEwciRQIm4UD+DsABewIH/QECdQCA EGQsAKhbkpACsJAaKTAHDvCAkbZeIdAJhgVTAgEHdLAJJQAHjgM5KZACOvg7WEUKnDAggvAKL/g8 z/OCtSghovAKgrB4+CQJmSMK5mYRbcCBxvhuRxBlb/E4LuRSIyGECbEIS5AFUSBW6CQFUrAEmSAQ pyAFUSAFSuBk36gEbGAQmNFhLAUAodAHBgACBBALI2AdbkcHmzYLBKAmKdAAJbCFs4AZCRBCHMcf pOYBCBCPpAAIA+AFAvCAHIABgPc3DxRCBygACRA44fI+DVAAqhQCArAjIMAABFAUUGFqAxECACAJ mHEmQ+EXZtGRe9CHAOj+FwrgHwLwSSDGTGoiEXDgFTzJIGSxFDzpFUwRlEFpgXvBAP/Bk6l2EXdw jE6JA0KgjGxhPYIgCoKALxEFEtC4EKkABDrAVjIQBaKCEGXgfWG1AzGACQlxjgZBQXtgaBTUByuB C27HB3+GfyCGQ3vAPWRHFH0AC/CxJ8xCagJDQXOQAg6AGQQzCnTQAB2kGIAwC1hIAqTwLXDgMesH C9ExCgpgADDBf5AyiXw1WgFFCgUwAKPQAQAwAnEjCYyoAPfCLdICgLmwMRIBid+Rm8iRkxfhCU/5 lEMglVqRCytHJ8E3E1upEF35lWEVlmmHEGZglm21jQghCepnEML1AQ7+YImruZA4xwdFQob3SJcC YBTpA3qQIwAHkEuk1gAIAAsKIDBwUy8lUJ77swmYwYkJEABmQ0Fx8D0SxD2C0ADjhYkEIZoCYQKk yYnvIwgACBOk4AAgciaw8AEL9C1zAHituBB94pNk4aG6GSJrVBFF8JtPWXnCqRWZg3ghkZwJcQri BJZRUAsJUZZs1U3UeRDyFwJ3w2oNMHe4oABbCDVyAAB7UI8DwAEe04WpsQceAyf2KAAw8T2klgB/ M3ASEkJPiguMKC/NZBafCQBwwH95JACx6H64IH+ptgFSxSz4uZoC4ZifsAECoBbcsyfHAwvv8xj2 eAAFsH4RoQGGF6L+hIpPXWgRYGCiTzkDMZE5/ZSCjcpPVwWEKbpiSRcRrNADzAl+T0CjZCmd2dQD aXkQceABGikIRUIAvwZ48oWFqsqlRloxIzA4DtAnDbAHjkkHfYKOgFdAS0E1HQmBf1MxIJAaHoAH ieGlmpQYalEHIZQLZ4EzaIEL8KEBzUMUrCIQvuUAHeAA0uqlAVAC79Nh++MkWBgHjOiZCBppgSQR DACi3wGvxyEWFnELNuCUQgAFWDAENOCUYVARudBPZfR8kTUSBDthlRqElwoRrBCj6BQEMtCpNQqq 2CSqamkQWFgAKoBDBYBKA7EHBOAAnAIHbyIQdSAAfTAKA8CKCsD+ActWniBgAHTQCQRAMLOgmgbg BW8mELPAAAUCC5uIQwYwAvfSFCNAAJsgCAIggLngAAqgCmxEAH6xCY4ZALOKC3yQGHAHeAQwliNA LEtBAiTbCX6QUJ/BATAhMGrRCQoQABrAAQ3AFQaQUszUUxEhr7vRACCQAm8wAnjbGrw5EWRwjE6A CgVBBmHGgTUgEfi1P2ekAWFjEAebXB9xsAVYGkcTUkjTa5obUpSbFS6KEF0pA2ylAzM6saU7qiZC Cr/WimPJsz8Bu5BDbKKCOO/yCrt0Uw9DCz+iZ5CTdr+GSWuTZ9y2bcQWvJiUvLkgKsKbC/IhvJL0 GLn7nLCALwL+KBENUKhHMQJXxTyTIAILwAALML7kW77me77jewGgsL6g4Anr677vy77uO7+goAol yoFjgBC0cL8beAgPIYjGdUaWSBCWGxOTSxuUdlkmwnMCDBehexCp4LDgtwSvaxA2GmtOl6NR4piA ir3a+1n9JAb8q6gkXMKRR28JUQuJK3lTsKEGkUMBrEgJkFIHbLCK1Gin8VgKXBAMTCceQKk+8cAG kQpMNlZAEJaeehAXfGuqKyVvsGgTkb2ECgIvOAqjYAUmnMVajANGwBCDu4FGsFMJkQv/GMOKpC8F bMNndLkTIYjOwjFYkcC4VRCtdEYgAMRBvLAPcQpR15xLkMT+BrEFFNt0TTwQtVAIiIzIiLDIibDI ipzIiADJjlwIkTzJlUzJlBzJmYzJmqzIl5zInpwImbzImtzJiJAIjTzKjIzIotzKpGzKmizKqtzJ oEzKnAzKiKwIl9AIiUwIsuMQUhyiPIaCf7DFxkzCZNAQ/Sp5NtBsCFHHZiwmSjWaZ4TDIFHDF6HD c5wV2oxa0CwmPywXQlwQRMyc26QDf5wQ0WnEZHWxBEELoCzLuDzPhSDP80zL9JzP+rzP/NzP/ozL 9vzP86wI9uzLDxHMuYkBKIhVVXDMDv2UvcMQScCBJ8AQtyUmIIArl2hZXPVeN2zAisTGEiHHfEYV wnVGftX+J4pkt3ExzgQRwZuKTRB7utA5yGipwVGG0N/hAVaMVULw0EDNgRWcEE7AgSKwEEVyRiwt ScUquR+txnQi0hHRzapXFSQ9MSp9RgnAomzh0gPBCn0MflGgC+ps0zHwTcKp09PBAQstCkQQ1HAd b7+8EEW9gRWtEFd9EC+TxhJBCpNSNJ9LzXQS2Dj01ymL14pU1bkiL0WzCS6M1CHF1QhB1Zh10QCw 1Q3h154rFV4tEGAd09gUlgqRBi1AfoUsN6wbgtQmu8YWYbarg7NQIrGdC63LKbMwCrZtzbB0URcF SyAoC782bGsjIZJ029LrUbH9CqIwC5ljIq/dE2o9r/z+hIJiENfWjQP+yxAjPHlfsBDdvNQKgc3/ pUjhTBCCwHpKbUsFcbDWjGPoTScKcEkFEc2XepXGBQIdLdhjMgpY+LiSXTmJjUOWPT8Mcd6QFXyt 0sMRWBCWTdgKy30OsZw3qgSEIMqEUAgXfuGIIAWgTcg4nR5FkwKAAAi0kAKcIEehAQdzAAhQsQcb ezZxkAsDzEZwJ+KzQMWjOAt+IOKkIAtPAwdO0o+CADUp8AmdAAjM5gecQHQPNQc/gp+bsAcCKIiT skvc23aSMOQoUDRmU38TpEluA92F+gZt/dPXHdRXwBCawIFFAMd6DVk83RB8PQtlbJzSRgoWGsMa kG3+7F0Q/GXGe/5R9P0TCg5ZZOjUZ5TVkOXgBHHSaBSbivQJSF3o/j0Qii4m7wJHdGJ8FdHZuACj m3pNQQAELFDqpn7qHc5NMuDOBSHjA5ECnCUHKa4vvsSJnwB3H+ABnCIIWAIHgultXtIH1xsLD1gH ftEJ/cdZhNgxfvGXHcOJ7+UlS8macRc32XpRP6d1csDsYl6oCDcKn/AJZn7mDx3RCTHukTcE7Y3o ivRzj+3R1SxJi2WcAEMKjmvGCaCDfI1D9x7DJ1Q/g17Z9K0a9HOwlq3ULdjN/QVZ05wQMBzN/55H itTR35yVrQbhDRFOqf59RnbWCAEL154CddAJnWD+AlDzn50wln3AiZ0Ad5tABx02CotIR+ECCNLy NY4t47PACbgyAiYgR3ETB3wwEMauWykwL50wq6OTUigQN3QQ5DMe5ZwoCCSwgofZ7YTqAM5jxaOg B1WwwvFGAxIgARMQAS7AgS4QAROABWzf9m7/9nBPBcsseYubEGFgjCi8ECRN3sp2EH2OcxPPw+1O B30wcDu8YIxFEPMuJpBb+AFeUAGPC5QewHd3sNFs8Qax95CV3wah4I1v+NssemKylO+dexbh6alg hBz/XDe6Aqz+EylgLCKOSb5OSnNAg2zEiYzJRqQkhqQERBMHRF/TCeXzlvbSAdcCB8pOd+nRAa/+ WwLMj+yzQHS6VQKP8fSq6G+A8AFq8TWzQAsrj/Uh2gDd22Pb/W4vMBYRwIEXoACHShFYYIw1QAkH cQXHmAoOcekHTuuITyfKof8A0QfXwIF9ABw8KJAgKQQIAYwiiKuEQwCdCBp0qHAgQ4cQcfXZE4ei hj4gFWJ0iIBOnzgNKWqUSBGAA0GjUCIsEVFnxFEyR+baqfMmAJikEnQcOEvmrIUUUwSFGhUXQqlV BxJisQPIVq5bfXQFGxYIizS1oM56OjCFxzgC4Vgk2Cknrk0C6QyMg4ETLjiACnLw2OcuLlhwcM0x TDBFLjhsNdJKOxAQCIJzBEYeWELSQDtAcaX+IIWLU59cceLgAnQaVyfMVl2/1tlAwWzatW3fpt1h 1ChRoniPqoFD+HDhL2pDIJ6cggIHsINSSh4dx5AxlDwd0nJDOvEhzje59HlQNcGJDjuRkjmeoAaH rXHRaR+xPEKPuNgjdA8fP0+KcyPeRygOz3DJZb6DNJCPogQGHAoB1zoJzyH/oAIQgPziG8iLlwjS DyG4nAuKKhB3IqQFrcTq6isUx0JDKlJCgGWgEQRR664U4KiJQzoE2QMWWBZT67I9BoLFvz1GAGQU WEZgKoU+NvFDkE1wmVGtORYCIcbK4pBSIFJAYIogTnD0Y0pcZgEBIjhEwSUWMP2YC5ARRqT+syrZ cMMTTxJ46+234KQzjjbktoNAgQbqJGiI7RZlVDhKQMylDxAiBIBG8ija5CgJd0LPITP5Q8jBS5HC pVOEPiWop1AjEqU/nUw9cMCBcqnwoVERGiwpmR6kCATwAowKVgBQHUjVg0TFxdiDKBtoUoQQRDQi EaPFBSsdVsT2hx5YMKOqXEjxDJYwX2EKllFCkbWTToCaBVydYJElqFlG6YQpd0udpROPSAlTXIK+ lTVZfWd9JWBYzovI3XthyWUWLWd5hVqJcXEgT4tte+O33nr7M7pAZxtUuuWaixaVRk9ObgpEZ5HU JwQGNBAATRHyIOCPKA0vzJjMKwhnn3T+bnXTiIaCiaA9NsyMovoG2tUqZQEoQdiEoBrKZwDiHahW ppTKaOKpDvIaqxOxFYuFFr1GO22110b7TjwZuHg2Ovj0zbeOk/tYgZCjW+5QasFAGWUbJu7EWU9v DS/Xoa2m6MOYA2Oc54EgFPqixoN6WqPHdWq6KspxwkUQn4jtOfKDPgRkw6F0pnZaicUmOyxuidSS bdtvx/12t+OmDe7aBPGNbuC2yzuCRfueWNHAGX10YlqR3hkhw0/dqWqf64sZLutxru/pCW8mVafP p05acoI6l8r7gTpEKIHQhDL9oPpymRkAyqb/PlrXX2eBBRhaAGAAWwCD/wWQBSs4Gyn+4MCH2uXO gQ+EIGx2x7vbNEBffdrY3YiTt70lp1Akk9gRlrcdRtDJZqVDiOYwVSsENBB80itBDGU4wxi+L3ry Q+GyaDhDEtQwVa6KiOi6Vj2KWOqGtpIWRXhVuZgBQAMBO5pDQLDDHdoQF3igiLD84rX9SSwRZVDD GtKwBjGqwYxpMCMZzeCGhSzQhRGEYxwjWDEK4oYBHdgY3UghhO24oDYXWJQFDIU2J4yQODRwBJ1I oQErEkQkQzziKKT2vcw5J2ZsShb0lgi6hP1EJ89ziA2zxzklOg2IBKoVAGoGqhSCiGvPSknauijH hTSSlrfEZdroWMfblIBuGyOFH2z+EJ0ZLKA2CZiAdCIwG795DXCGPEIsFOkS0iTsV0iM5AtbGZFf IaCRuVhLUEY5kF+575Ph3MnToPUfpwwInCNJUPiYVkrPnbJU1wSAenBRzm+m4EM7SUGEFCexWebS oAdFKKImyMvZZExjpOjTJ76wAIpSNAG2gVtFF2ABC/iOOWpDhfJOdoMx1IkjoXJSHwJKEWQdES7T w6HRWNoHaeYCEPehQ8A2N5Aotq8PTLEpTgMmNYV4RIgpaUtLZGJEl5LSIa5RXxB9ApMG9SFGuRCE UIOyiQjZUn9gS2hYxTpWOzH0NnjA4C9F0Qez2qaZaQOFFpCwnRsM4Q6IOinjYLL+U6kpwJ2pDA8C lnbJfwHWZUvbZ3j844HIfY+wSXyqKSvHIZ8YMRcYYJxggwJYZqGtoGQFbWhxuVCGAg+iffrNKDbR 1tqAkG2TGIMVqEAFK4AhD9KM1iIj19nyechyTrGm1Z4YT98uBJ8+GW5EHikT/+QCpuFZJXFjej56 SmV8UBOnT2yYV0olF34y2aJnwSpa8pZXjqStowY25htS6HEUHGAtM80blHf6DAQ6VVpEnjskLOEs uojD5kY+4F+b5eK4EyoQzoCkk3FSN7JVieonGasgnZFiwhECE1Tox1JbqO2z8wVxiKmFXgpqIA5y kAMc4PAGFMshDm+YA3zj61r+ESdrpcgtWm9PF5FZ4LORgniuE5kK4E8EBcjIHTKPmzgnnWyiiVBD LIARi76oXDd/G8HnfYMYZA0kWSc3Fs/aPlxjMpdZKiSOb5oraOZ/1UWGTipy7mZRl5JsgnUro/OT 7nwWQZSkD6M44ZnybGc2E2TOfia0VWoVZYKOt9CPhvRA0Mw7BnhUzbXx3VsjvWlOg5arDlmnLB3d aVLPNwCnRnWqAZBqVqP6IAJY9alj3epVw7rVtHZ1qXW9a1o6V5O8BnawB7IJSRBbEscutrGVTWxm H3sTzxZEsZNtbGlPO9rPdra0lV1sQfxT2N8GN6RSmspAh9vc50Z3utVNy6fHOUSf64Z3vOU9b3pX 5dMyeVm99R3aXADF3/0GSodt0WFcdLjfBDo4whF+cIYnXOAEN/i/Z7Vvikd6e3GueMZx2QEAGMDj Hwd5yEU+cpKX3OQGcKLGVV5j9h3IqyuH+e3QYwAFIMDmN8c5Ag6Qc5733Oc533nPax7gmBd9rJug gwzpQDqjN11tqSOApS+tZgIAgL9Ox3rWtR5sUoygAV8He9jF3gAHOGDsZ0d72sMeAkZv3e1vh3vc 5T53utfd7nfHe971jouAAAA7 ''' self.open_lock_icon = '''\ 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 By988Hf0cVFAADs= ''' self.closed_lock_icon = '''\ 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 ''' self.key_icon = '''\ R0lGODlhMAAwAPcAAAABAAkFAgQLBg0IAwwNCxMMBQwQCBgRBhEVChQYCxsYChISEhwcHCIXCSUb CyseCzgVBC8hDDEiDTgnDz0tEiYmJi0tLTQ0NDs7O0YdB00iCVYmC1wqC0o1FFY7Flg9F1k/GGsx DlxBGWBDGmZJHGlNHm5QHnRRH0RAOnVUIXpVIEVFRUxMTFxZVlxcXGJiYmpqanFuZHNzc3l5eZNG FaVQGINbI4NhJo1jJpJmJ5BnKJVrKZ5sKbVuJ6N1LbJyKrl2K7B5L7J9ML5/MchiHsN8LPN3JPl8 JrSBMryDMr+MNcyALtWDLsGFM8mNNtWWOtSYOtibPP+BJ/+FKP+JKv+SLeOMMuyPM+GcPO+cOueh PuyiPvGjPqqbeu2mQPOnQPSqQfqtQvGzRP60Rf+5R/i3SPe8SP69Sf/ES//LTv/RT//RUf/cVP/q Wf/0Xf/udIWFhYiIiJKSkqWlpKysrLOzs7u7u83EkP/0nMnDp8XFxcvLy9/f3+Tk5Ovr6/Xv6PX1 9Pr6+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAIIALAAAAAAwADAA AAj+AAUJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3MixY0Y6L1asgKHHo8IZAABI+ODBAYAF dEwW9FMBAA4uZNCsQRNGBAAYMgUCYqBAyxoyY9BgGWKmDQ8AcYKyKCAGDZgwaEikBHDDTQ4Ae0zu AeAEDRcwaHYACEFkAwAhbB4A9eiCAhoxXMaQEZChypQqEBCw0UGgJEdABHSkAXN1DAAOVYxU0SBA DRQAcjqOdXKGMdoUAGocoQHAhpotAaJyHPukM+MxZShs9YAGzeXMHPkASGKV8ZekQkzwTuoDQJ2O gQyQ6O0bq5reaDqsCNQRDw8erj1r54ImCoCYHLv+YKn9Rft2NGQcXOjogixzNGfCeCazZssDAn44 sgDQowoYMfKp0ERttYVhAwAM5LfRfkBQccUZtY3hgAATePBBBwUAMAMgHK0AQBFUZIGGEx6o8AYg e8ThggszzKHgRisIAGIWEJ4AwAV/BDUQBggsEWIZaKmAgY47EgDiFmiUUQYbJqxHpAUIMBHiGV5g MYYQP+kISAUJWEGFF2ngAEAAGbqgZQUGXPEjGiMwMIccc+jYxwUJqLnFGWOsAYKTRNIBQINedIak BxYQKVAdAPyAJxgRjAkACoYKgigSanQWQAx92NFHpIgGAd8XANwR6UCIHuCAAwcAkMeoAvUBxwsU MswwwwubsmrrrbjmquuuvPbKUUAAOw== ''' self.balloon_icon = '''\ R0lGODlhMAAwAPejAAAAAAICAgMDAwQEBAUFBQgICAkJCQoKCgsLCwwMDA0NDQ8PDxAQEBERERQU FBUVFRYWFhgYGBsbGx0dHR4eHh8fHyEhISMjIyUlJSYmJicnJygoKCsrKy0tLTAwMDIyMjMzMzQ0 NDc3Nzg4ODo6Ojw8PD09PT8/P0JCQkdHR0hISEtLS0xMTE1NTU9PT1BQUFFRUVJSUlRUVFVVVVZW VldXV1lZWVpaWltbW11dXV9fX2BgYGFhYWJiYmVlZWZmZmhoaGlpaWpqamtra21tbW5ubm9vb3Jy cnNzc3Z2dnh4eHt7e3x8fH5+fn9/f4CAgIKCgoODg4SEhIaGhoyMjI2NjY6Ojo+Pj5CQkJSUlJaW lpeXl5mZmZubm52dnZ6enqGhoaKioqOjo6SkpKenp6mpqaqqqqurq6ysrK2tra6urq+vr7CwsLGx sbOzs7S0tLa2tre3t7m5ubu7u7y8vL29vb+/v8HBwcLCwsPDw8TExMXFxcbGxsnJycrKysvLy83N zc/Pz9DQ0NLS0tTU1NXV1djY2NnZ2dra2tvb29zc3N/f3+Dg4OHh4ePj4+fn5+jo6Onp6erq6uzs 7O3t7e/v7/Dw8PHx8fLy8vPz8/T09Pb29vf39/n5+fr6+vv7+/z8/P39/f7+/v///wAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKQALAAAAAAwADAA AAj+AEkJHEiwoMGDCBMqXMiwocOHECNKnEiRVKAvVbr4qTgRlBscCgCINCAjTSeODvVUIMDji59D gML8OBBBDsqFUQDsqDSqp89RmoQASHITIRYAYX4q7bkGgJOiBeEAELO0ahsAZaAO3MCjqtcjETZp VQPgkdeqmBqI0YrDx1mvRlYYyuIFEcUyHhgA+PK26hkAASYsAACBDMRNIQD84GOoU9+lmvpACuXp ERMANBx+ytBh0ePPPu8AMNLQRgRKoFOPmgNgo8JFAPKoVt3iRiOFVCh4mp0azAMvCmG45Q1aUAAp ClEgIQ4akIApCn2kYP55i4S1Ca9Got63RJFLCy3+POF+9guARAzHACBDfmkaANAbOtnbvuefFQCa QJwhpP6kABcYEpEOQdQ3ygpFSKSECAYO8YJEdAAwSH0q+DCRBle0BxsaEy1RgFncdYFBRR/k8Jko opxFSEWEELDDYzw08IdPfXxQgBCQoHTHACYg4tUiKmggAwBbjOIFABv0AIEDe6D0CAkA+JBGIZZY YkgbQADwASOkxJGJIgBY4dMQGhQFRw0IACCAAAAUAMMbBTEBwk+NAMAGVJzgAQYYdmRyUAw9/JRJ AlxoxdAVAUASGgCFGLoQKBWwgEgldUSQAymPmCGJowg1koFIALhAiiVpRoAJpwgNsoYjAhHRwigb J0CBKkNOjCAIB1rMyhALANig66/ABitsUQEBADs= ''' self.shield_icon = '''\ R0lGODlhMAAwAPcAAAEAAAwBAQAGCAALDQsLCxIAABgAAAAPEgAQEwQdHhQUFB0dHSQBASwBADIB AD0OBxQsLRMjJQ8wMRsyNRk1OCQkJCsrKzAvLjAwLyUyNDMzMzg3Nz08PEQDAFwKBlULCFcGA08U DVEUDVoXDmMIA2sKBXANCHQUD34RDGYbEXwmGiZCRjpRUkRDQ09OTkpcXFNTUlhYV1tbWmBgX0Rl Zk9lZlJgYVJsbllzdGRkY2pqamJzdXJxcHV6enx7e4gTD44XEpMZEqojHLkkHZYuIYwxI5czJaU6 K8sjHs4nIc0qI9knIdIvJ9ovJ9EvKNIwJ9MyKtwxKdg9Mu0sJuQzK+s1LPM3Lfg3Lvc4Lvs4L/87 MbdCM8pMO99JPOBNPt9WRORUQ+dYRupaR+xdSfVSQ/9WRfVdSfxbSe5iTfRgTPxiTf9mUf9rVP9v WP9yWnaAgXyLjYOEg4yLioCSk4uRkYSYmJSTk5ubm5SdnZqjpJ2oqKSkpKOop6mpp6eqqqurq6ez srOzs7O9vb69vcC/v77Dw8PEw8jGxsnIx8vLytHQz9TU09jW1dvZ19zc3ODf3t/g3uPj4+rn5uvp 5+rq6fDv7vTz8/7+/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAJgALAAAAAAwADAA AAj+ADEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGA06GmRHR4wWLVzkkBNokaWMBxfFqQCAQAUO LmTI+IhBAQAFOQahFLjIBYAKOghZquRokdFFjihZUqkBgIU/Ge847XPJUqEdEw4A2ArgAAQbgChd 6glAxkU7AHRcutRjwFYHKYoQAYKChAOuLyBd+gOgRUVHAHJcOoQAwAguatawaVNmipXHVJSY2Brn UiAAdijqWDAYAIMvbNaYESNmzJgnTFJHoZKFigcAPS7JqECRQ4xLEQqEFhNmDGkxo1GnTk1FCwgA kezQnhjDRSIARNiEKU3dNBopqYUziQxAUBwNFH3+cDgEwAib3+jHeFEyPDuVId1n+J3Ix4IiBCnO oxeDxkuS9kygZkUQABTSgg4UIRLAIhN0oB96aITxH4BMXFHCAIdYcAdFjwAwyA0NrOEbdaT1x95w AnYggSIEHELRJQrQAQcAaoy23xhpSMEeagIycAMhADxSUQwx5AEAYvvxFwaFqwEwhxwKXFKRHRaQ p8KDpPmW3ZZWCAEAHzGYVdEiACSyggNYjtFfEk4EOJxxAySyQB8WWRLAHXPQaONvY0iRhHZMWFEA DeQ5cpEMOZB3hHQQjgGFE5AqwZ0eciyA0SAEODIBmjei8egTkGrxmiMcxIHRJQTcYWQYavhmWp/4 EzIRRRUA4EBeJBnpoAElAqTQxnT8dcFmak5oAQQAi+iK0nOHzCiGGtP5xqOsVgTAgiQB5LETBy1c AoAIbowxHRqnpabFCcj5oMBJKJGHCB4AbPGrdX5SwQQANlgiQGY7YdKCBZdkAIAZa4SBhrCsMTCA bFH2iwlgd3jbABtsrNeEFh+USSZUDmMSBwCVkBeCG2QscS4AdFxSwXwdY7IAwHwA8MAZosImGwCU tMwTALddFgADAFT2MZ06C9RHWmNBkEAgl6DlQ9EEocVDVZFcIhWCUEfNsyWX+JBW1gb9QYAGLQQN 9kGRcGBBIme37fbbcMct99xQBwQAOw== ''' self.double_exclamation_icon = '''\ R0lGODlhKAAoAPcAAJcbD5QbEZkcEL4cDq0fE50lGZ8pHaAqHrQhFLsiFbYmGpguJZoyKpk4MMId D8EeEMQkFsslF8onGMIoG84oGdUpGtwrGtEwH+MtHOouHPEtGuwwHvAxHsYuIcgzJcs3KdQyItk3 KN87Kt0+MO80IOM/LvE2IfQ4I8pAM8pGOs1LP9RCNd9DNNVGONlLPeRBMKBQSrVTS61eV7peValo Y8FTSc1XTN9TRNRWSd1WStdbTs1bUNleUvJbR9ZnXd9mWuBqXvFpWvhwW9FsY9hsYdtxZ9N0a9l/ dut1Zut2aOt6bPp3Yvl8aOF8cseEftCEfd2FfeOCePGEeLmGgsiGgdmHgNyLg9CPiN2TjN6cleGN heuKgOWUjOOZkuqbkvSclN63tNW4ttm/veimoOOqpeurpeavquyxq+i1sO67tuS9uu2+uu3DvvnE v9/KyOHEwezGw+7JxfDHw/HLx/zKxPzPyfLRzfzRzPLV0vPZ1/Xc2v3b2O/k4/Pm5fjm5P7o5vbt 7Pnt6/bw7/bx8Pvz8vz49/36+gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAIcALAAAAAAoACgA AAj+AA8JHEiwoMGDCBMqXMiwocOHECNKnAhxT5s6dO7cqVPnTsaChuhk1MiRIx0/DL/0kMKkpUsh UgoWCuKy5pIgbRh6wWDCBAkTG06c4BDF4A0THIAOFXrDDsM0IjRoyEA1w1QvBoFsmFp1KhCUC+3k IJGBhIYNGzhgOGMwSlq0GbZmiGKIoZ8fZami3YAhjkEue/dy2MClIaEmGOJmwIC2Qh6DZRhXjYuh jEMuGMiSSEwiRCCDbCxQJUvVAhuHY0RjSMzYxcE8F0iojnsBj8PQm1cn5nEQEIgNGUSLBvG5oZ0K unUXOWioRWnRGVrUbejnQnIMFrQg1IE9uY6Hhkb+YLdgAXsXhER0l7dABCIO8t0roEEIZf16KBB9 kIdfAQ7CLhXsZ0EF5z0ERYAVIFeBHgiZEeB+FZgBERYIBugBIQjBgSB5FPj3EBkVVoBDQnhIMGCA ETz2kBoJtrhDQoB4cGIFHgACUYktUmCEQim0WEEKEfnhQY5WKLSDjy9CZAgKM2ahEBE+thfRDAEc UIAAaih0RQAGHADAFRIRMoghhBAyHUKFCGKIIYIUQtGbEemxBRJKWKZQIF4kgYQXxT00BwslgADC C00kRIgOL4BQQQk5YPgQDxUMICkEFWR5kBYVPPDAAA5UoJ1DfkwAgQQQQJBABEUeZEOprEKQJHVT CrRa6hMI1SBrBDFAFAMEFCQAQQQEgIEQFQhQUGoECTgBkRsBIEAqATLAyAACpSLAgI3LwrAAA1Oc 2RsNDDBAA1gR9SHGG94iNIgYYggC57sFBQQAOw== ''' self.master_version = master_version self.admin_password = admin_password self.fwpw_status = fwpw_status self.hashed_key = None self.obfuscated_keys = None self.obfuscated_string = None self.cleared_keys = None self.postinstall_script = None self.plaintext_keys = None self.key_item = '' self.keys_loaded = False self.key_source = "" self.state_button_state = "disabled" self.hash_button_state = "disabled" self.previous_keys = [] self.current_key = '' self.config_options = {} self.injest_config() self.remote_username = StringVar() self.remote_password = StringVar() self.remote_hostname = StringVar() self.jamf_username = StringVar() self.jamf_password = StringVar() self.jamf_hostname = StringVar() self.jamf_hostname.set("https://jamf.pro.server:8443") self.jamf_username.set("") self.jamf_password.set("") self.hashed_results = StringVar() self.fwpm_package_dest = StringVar() self.signing_cert = StringVar() self.keyfile_loc = StringVar() self.status_string = StringVar() self.fwpm_package_dest.set("/") # self.status_string.set(u'\U0001F923'.encode('utf-8')) self.status_string.set("Ready.") self.fwpw_enable = IntVar() self.fwpw_enable.set(0) self.reboot_enable = IntVar() self.reboot_enable.set(0) self.include_config = IntVar() self.include_config.set(0) self.use_slack = IntVar() self.use_slack.set(0) self.slack_identifier = StringVar() self.slack_url = StringVar() self.slack_info_url = StringVar() self.slack_info_channel = StringVar() self.slack_info_bot = StringVar() self.slack_error_url = StringVar() self.slack_error_channel = StringVar() self.slack_error_bot = StringVar() self.state_string = StringVar() self.state_string.set('Firmware password is ' + self.fwpw_status) self.keys_loaded_string = StringVar() self.keys_loaded_string.set('No keys in memory') self.logger.info(self.state_string.get()) self.logger.info(self.keys_loaded_string.get()) if self.config_options: if self.config_options['slack']['slack_info_url']: self.slack_info_url.set(self.config_options['slack']['slack_info_url']) if self.config_options['slack']['slack_info_bot_name']: self.slack_info_bot.set(self.config_options['slack']['slack_info_bot_name']) if self.config_options['slack']['slack_info_channel']: self.slack_info_channel.set(self.config_options['slack']['slack_info_channel']) if self.config_options['slack']['slack_error_url']: self.slack_error_url.set(self.config_options['slack']['slack_error_url']) if self.config_options['slack']['slack_error_bot_name']: self.slack_error_bot.set(self.config_options['slack']['slack_error_bot_name']) if self.config_options['slack']['slack_error_channel']: self.slack_error_channel.set(self.config_options['slack']['slack_error_channel']) if self.config_options['slack']['slack_identifier']: self.slack_identifier.set(self.config_options['slack']['slack_identifier']) if self.config_options['slack']['use_slack']: # translate into 0/1 for false/true self.use_slack.set(1) self.slack_optionator() else: self.use_slack.set(0) if self.config_options['keyfile']['path']: self.keyfile_loc.set(self.config_options['keyfile']['path']) self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) self.root.geometry("604x500") self.logo_photoimage = PhotoImage(data=self.logo) self.closed_lock_icon_photoimage = PhotoImage(data=self.closed_lock_icon) self.open_lock_icon_photoimage = PhotoImage(data=self.open_lock_icon) self.key_icon_photoimage = PhotoImage(data=self.key_icon) self.balloon_icon_photoimage = PhotoImage(data=self.balloon_icon) self.shield_icon_photoimage = PhotoImage(data=self.shield_icon) self.double_exclamation_icon_photoimage = PhotoImage(data=self.double_exclamation_icon) self.superframe = ttk.Frame(self.root, width=604, height=525) self.superframe.grid(column=0, row=0, sticky=(N, W, E, S)) self.logoframe = ttk.Frame(self.superframe, width=604, height=90) self.logoframe.grid(column=0, row=0, sticky=(N, W, E, S)) self.logoframe.grid_rowconfigure(0, weight=1) self.logoframe.grid_rowconfigure(2, weight=1) self.logoframe.grid_columnconfigure(0, weight=1) self.logoframe.grid_columnconfigure(2, weight=1) self.logo_label = ttk.Label(self.logoframe) self.logo_label['image'] = self.logo_photoimage self.logo_label.grid(column=1, row=1, sticky=(N, S, E, W)) self.stateframe = ttk.Frame(self.superframe, width=604, height=30) self.stateframe.grid(column=0, row=1, sticky=(N, W, E, S)) self.stateframe.grid_columnconfigure(0, weight=1) self.stateframe.grid_rowconfigure(0, weight=1) self.stateframe.grid_columnconfigure(2, weight=1) self.stateframe.grid_rowconfigure(2, weight=1) self.lock_label = ttk.Label(self.stateframe) if self.fwpw_status == 'On': self.lock_label['image'] = self.closed_lock_icon_photoimage else: self.lock_label['image'] = self.open_lock_icon_photoimage self.lock_label.grid(column=0, row=1, sticky=(E)) self.state_label = ttk.Label(self.stateframe, textvariable=self.state_string, font=("Helvetica", 24)) self.state_label.grid(column=1, row=1, columnspan=1, sticky=(W)) self.keys_label = ttk.Label(self.stateframe) self.keys_label['image'] = self.balloon_icon_photoimage self.keys_label.grid(column=0, row=2, sticky=(E)) self.keys_loaded_label = ttk.Label(self.stateframe, textvariable=self.keys_loaded_string, font=("Helvetica", 24)) self.keys_loaded_label.grid(column=1, row=2, columnspan=1, sticky=(W)) ttk.Separator(self.stateframe, orient=HORIZONTAL).grid(row=10, columnspan=3, sticky=(E, W), pady=0) self.navframe = ttk.Frame(self.superframe, width=604, height=30) self.navframe.grid(column=0, row=10, sticky=N) self.master_pane() def master_pane(self): """ The home pane. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) self.logger.info("%s" % inspect.stack()[1][3]) self.mainframe = ttk.Frame(self.superframe, width=604, height=510) self.mainframe.grid(column=0, row=2, sticky=(N, W, E, S)) self.mainframe.grid_rowconfigure(0, weight=1) self.mainframe.grid_rowconfigure(5, weight=1) self.mainframe.grid_columnconfigure(0, weight=1) self.mainframe.grid_columnconfigure(2, weight=1) self.change_state_btn = ttk.Button(self.mainframe, width=20, text="Change State", command=self.change_state) self.change_state_btn.grid(column=0, row=80, pady=4, columnspan=3) self.change_state_btn.configure(state=self.state_button_state) self.info_status_label = ttk.Label(self.mainframe, text='Location of keyfile:') self.info_status_label.grid(column=0, row=90, pady=8, columnspan=3) ttk.Button(self.mainframe, width=20, text="Retrieve from JSS Script", command=self.jss_pane).grid(column=0, row=100, pady=4, columnspan=3) ttk.Button(self.mainframe, width=20, text="Fetch from Remote Volume", command=self.remote_nav_pane).grid(column=0, row=200, pady=4, columnspan=3) ttk.Button(self.mainframe, width=20, text="Retrieve from Local Volume", command=self.local_nav_pane).grid(column=0, row=300, pady=4, columnspan=3) ttk.Button(self.mainframe, width=20, text="Enter Firmware Password", command=self.direct_entry_pane).grid(column=0, row=320, pady=4, columnspan=3) ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=400, columnspan=3, sticky=(E, W), pady=8) hash_display = ttk.Entry(self.mainframe, width=58, textvariable=self.hashed_results) hash_display.grid(column=0, row=450, columnspan=4) self.hash_btn = ttk.Button(self.mainframe, width=20, text="Copy hash to clipboard", command=self.copy_hash) self.hash_btn.grid(column=0, row=500, pady=4, columnspan=3) self.hash_btn.configure(state=self.hash_button_state) ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=700, columnspan=3, sticky=(E, W), pady=8) self.status_label = ttk.Label(self.mainframe, textvariable=self.status_string) self.status_label.grid(column=0, row=2100, sticky=W, columnspan=2) ttk.Button(self.mainframe, text="Quit", width=6, command=self.root.destroy).grid(column=2, row=2100, sticky=E) def jss_pane(self): """ JAMF server interaction pane. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) self.mainframe.grid_remove() try: if self.config_options["keyfile"]["remote_type"] == 'jamf': if self.config_options["keyfile"]["server_path"]: self.jamf_hostname.set(self.config_options["keyfile"]["server_path"]) if self.config_options["keyfile"]["username"]: self.jamf_username.set(self.config_options["keyfile"]["username"]) if self.config_options["keyfile"]["password"]: self.jamf_password.set(self.config_options["keyfile"]["password"]) except: pass self.jss_frame = ttk.Frame(self.superframe, width=604, height=510) self.jss_frame.grid(column=0, row=2, sticky=(N, W, E, S)) self.jss_frame.grid_columnconfigure(0, weight=1) self.jss_frame.grid_columnconfigure(1, weight=1) self.jss_frame.grid_columnconfigure(2, weight=1) self.jss_frame.grid_columnconfigure(3, weight=1) # 52? beam_a = ttk.Button(self.jss_frame, width=5) beam_a.grid(column=0, row=0, sticky=W) beam_b = ttk.Button(self.jss_frame, width=20) beam_b.grid(column=1, row=0, sticky=W) beam_c = ttk.Button(self.jss_frame, width=10) beam_c.grid(column=2, row=0, sticky=W) beam_d = ttk.Button(self.jss_frame, width=16) beam_d.grid(column=3, row=0, sticky=W) beam_a.grid_remove() beam_b.grid_remove() beam_c.grid_remove() beam_d.grid_remove() ttk.Label(self.jss_frame, text="Download keys from Jamf Pro FWPM script:").grid(column=0, row=100, columnspan=4, sticky=(E, W)) # ttk.Separator(self.hash_frame, orient=HORIZONTAL).grid(row=120, columnspan=50, sticky=(E, W)) ttk.Label(self.jss_frame, text="Server:").grid(column=0, row=150, sticky=E) hname_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_hostname) hname_entry.grid(column=1, row=150, sticky=W, columnspan=2) ttk.Label(self.jss_frame, text="Username:").grid(column=0, row=200, sticky=E) uname_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_username) uname_entry.grid(column=1, row=200, sticky=W, columnspan=2) ttk.Label(self.jss_frame, text="Password:").grid(column=0, row=250, sticky=E) pword_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_password, show="*") pword_entry.grid(column=1, row=250, sticky=W, columnspan=2) ttk.Button(self.jss_frame, text="Find Script", width=15, default='active', command=self.search_jss).grid(column=1, row=300, columnspan=2, pady=12) ttk.Separator(self.jss_frame, orient=HORIZONTAL).grid(row=1000, columnspan=50, pady=12, sticky=(E, W)) ttk.Button(self.jss_frame, text="Return to home", command=self.master_pane).grid(column=2, row=1100, sticky=E) ttk.Button(self.jss_frame, text="Quit", width=6, command=self.root.destroy).grid(column=3, row=1100, sticky=W) def direct_entry_pane(self): """ Directly enter fwpw. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) self.current_key = tkSimpleDialog.askstring("FW Password", "Enter firmware password:", show='*', parent=self.root) if self.current_key: self.keys_loaded = True self.calculate_hash() self.status_string.set('Keys loaded successfully.') self.keys_label['image'] = self.key_icon_photoimage self.keys_loaded_string.set('Keys in memory.') self.change_state_btn.configure(state="normal") else: self.flush_keys() self.status_string.set('Blank password entered.') self.logger.error('Direct enter blank password.') self.change_state_btn.configure(state="disabled") def remote_nav_pane(self): """ Connect to server and select keyfile. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) self.mainframe.grid_remove() try: if self.config_options["keyfile"]["remote_type"] == 'smb': if self.config_options["keyfile"]["server_path"]: self.remote_hostname.set(self.config_options["keyfile"]["server_path"]) if self.config_options["keyfile"]["username"]: self.remote_username.set(self.config_options["keyfile"]["username"]) if self.config_options["keyfile"]["password"]: self.remote_password.set(self.config_options["keyfile"]["password"]) except: pass self.remote_nav_frame = ttk.Frame(self.superframe, width=604, height=510) self.remote_nav_frame.grid(column=0, row=2, sticky=(N, W, E, S)) self.remote_nav_frame.grid_columnconfigure(0, weight=1) self.remote_nav_frame.grid_columnconfigure(1, weight=1) self.remote_nav_frame.grid_columnconfigure(2, weight=1) self.remote_nav_frame.grid_columnconfigure(3, weight=1) ttk.Label(self.remote_nav_frame, text="Read keyfile from remote server: (ie smb://...)").grid(column=0, row=100, columnspan=4, sticky=(E, W)) ttk.Label(self.remote_nav_frame, text="Server path:").grid(column=0, row=150, sticky=E) hname_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_hostname) hname_entry.grid(column=1, row=150, sticky=W, columnspan=2) ttk.Label(self.remote_nav_frame, text="Username:").grid(column=0, row=200, sticky=E) uname_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_username) uname_entry.grid(column=1, row=200, sticky=W, columnspan=2) ttk.Label(self.remote_nav_frame, text="Password:").grid(column=0, row=250, sticky=E) pword_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_password, show="*") pword_entry.grid(column=1, row=250, sticky=W, columnspan=2) ttk.Button(self.remote_nav_frame, text="Read keyfile", width=15, default='active', command=self.read_remote).grid(column=1, row=300, columnspan=2, pady=12) ttk.Separator(self.remote_nav_frame, orient=HORIZONTAL).grid(row=1000, columnspan=50, pady=12, sticky=(E, W)) ttk.Button(self.remote_nav_frame, text="Return to home", command=self.master_pane).grid(column=2, row=1100, sticky=E) ttk.Button(self.remote_nav_frame, text="Quit", width=6, command=self.root.destroy).grid(column=3, row=1100, sticky=W) def local_nav_pane(self): """ Select keyfile from local volume. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) self.key_item = tkFileDialog.askopenfilename(title="Local object", message="Select local object:", parent=self.root) if self.key_item: self.status_string.set('Object found.') self.handle_key_item() else: self.status_string.set('No object selected.') def search_jss(self): """ Search the JAMF server for FWPM Control script, strip out and categorize keyfile. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) try: jss_search_url = self.jamf_hostname.get() + '/JSSResource/scripts' headers = {'Accept': 'application/json', } response = requests.get(url=jss_search_url, headers=headers, auth=requests.auth.HTTPBasicAuth(self.jamf_username.get(), self.jamf_password.get())) script_list = response.json() except requests.exceptions.HTTPError as this_error: self.logger.error("http error %s: %s\n" % (response.status_code, this_error)) if response.status_code == 400: self.logger.error("HTTP code {}: {}".format(response.status_code, "Request error.")) elif response.status_code == 401: self.logger.error("HTTP code {}: {}".format(response.status_code, "Authorization error.")) elif response.status_code == 403: self.logger.error("HTTP code {}: {}".format(response.status_code, "Permissions error.")) elif response.status_code == 404: self.logger.error("HTTP code {}: {}".format(response.status_code, "Resource not found.")) for item in script_list['scripts']: if 'FWPM Control' in item['name']: target_id = item['id'] script_url = self.jamf_hostname.get() + '/JSSResource/scripts/id/' + str(target_id) headers = {'Accept': 'application/json', } response = requests.get(url=script_url, headers=headers, auth=requests.auth.HTTPBasicAuth(self.jamf_username.get(), self.jamf_password.get())) response_json = response.json() if response.status_code != 200: self.logger.info("%i returned." % response.code) return working_output = response_json['script']['script_contents'].split('\n') self.previous_keys = [] for line in working_output: if "'previous':" in line and '#' not in line: try: contents = re.findall(r'\s*\'previous\': \[(.*)\]', line) if contents: in_contents = contents[0].split(', ') in_contents = [i for i in in_contents if i] for item in in_contents: subitem = item.split('"') subitem = [i for i in subitem if i] subitem = [i for i in subitem if i != ','] if subitem: self.previous_keys.append(subitem[0]) except Exception as exception_message: self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) elif "'new':" in line and '#' not in line: try: contents = re.findall(r'\s*\'new\': (.*)', line) if contents: if len(contents) == 1: contents = contents[0] else: quit() subitem = contents.split('"') subitem = [i for i in subitem if i] # self.current_key = subitem[0] self.current_key = subitem[0] except Exception as exception_message: self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) try: self.calculate_hash() self.status_string.set('Keys loaded successfully.') self.keys_loaded_string.set('Keys copied to memory.') # self.hash_button_state # self.change_state_btn.configure(state="normal") except Exception as exception_message: self.logger.error(exception_message) self.flush_keys() # self.change_state_btn.configure(state="disabled") def local_fetch(self): """ Popup simple local navigation dialog. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) self.key_item = tkFileDialog.askopenfilename(title="Local object", message="Select local object:", parent=self.root) if self.key_item: self.status_string.set('Object found.') self.handle_key_item() else: self.status_string.set('No object selected.') def read_remote(self): """ Handle server connection, keyfile selection and remote volume dismount. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) tmp_directory = "/tmp/sk/mount" if not os.path.exists(tmp_directory): os.makedirs(tmp_directory) try: self.logger.info("%s: %s" % (inspect.stack()[0][3], "Mounting")) msb.mount_share_at_path_with_credentials(self.remote_hostname.get(), tmp_directory, self.remote_username.get(), self.remote_password.get()) self.key_item = tkFileDialog.askopenfilename(initialdir=tmp_directory, title="Remote object", message="Select remote object:", parent=self.root) self.logger.info("%s: %s" % (inspect.stack()[0][3], self.key_item)) if self.key_item: self.status_string.set('Object found.') self.handle_key_item() else: self.status_string.set('No object selected.') self.logger.info("%s: %s" % (inspect.stack()[0][3], "Dismounting")) umount_results = subprocess.check_output(["/usr/sbin/diskutil", "unmount", tmp_directory]) self.logger.info(umount_results) except Exception as exception_message: self.logger.error(exception_message) def calculate_hash(self): """ Builds hash identical to FWPM binary. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) try: hashed_key = hashlib.new('sha256') hashed_key.update(self.current_key) for entry in sorted(self.previous_keys): hashed_key.update(entry) fwpw_managed_string = hashed_key.hexdigest() self.hashed_results.set(fwpw_managed_string) self.keys_label['image'] = self.key_icon_photoimage self.hash_btn.configure(state='normal') self.change_state_btn.configure(state='normal') self.state_button_state = 'normal' self.hash_button_state = 'normal' except Exception as exception_message: self.logger.error(exception_message) self.flush_keys() def copy_hash(self): """ Provides single button to copy hash to clipboard. """ self.logger.info("%s: activated" % inspect.stack()[0][3]) os.system("echo '%s' | /usr/bin/pbcopy" % self.hashed_results.get()) def handle_key_item(self): """ attempts to open and parse selected keyfile plain text obfuscated inside dmg --someday inside encrypted dmg --someday """ self.logger.info("%s: activated" % inspect.stack()[0][3]) self.flush_keys() if os.path.exists(self.key_item): item_filename = self.key_item.split('/')[-1] item_extension = item_filename.split('.')[-1] if item_extension == 'plist': passwords = [] try: keyfile_plist = plistlib.readPlist(self.key_item) content_raw = keyfile_plist["data"] content_raw = base64.b64decode(content_raw) content_raw = content_raw.split(",") content_raw = [x for x in content_raw if x] for item in content_raw: label, pword = item.split(':') pword = base64.b64decode(pword) if label == 'new': self.current_key = pword else: self.previous_keys.append(pword) except Exception as exception_message: self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) return elif item_extension == 'txt': try: with open(self.key_item, "r") as keyfile: passwords = keyfile.read().splitlines() for item in passwords: label, pword = item.split(':') if label == 'new': self.current_key = pword else: self.previous_keys.append(pword) except Exception as exception_message: self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) return else: self.logger.error("%s: Error parsing keyfile." % (inspect.stack()[0][3])) self.calculate_hash() else: # print('no key item') # print(self.key_item) pass def injest_config(self): """ attempts to consume and format configuration file """ self.logger.info("%s: activated" % inspect.stack()[0][3]) running_pathname = os.path.abspath(os.path.dirname(sys.argv[0])) self.logger.info("%s: Application pathname: %s" % (inspect.stack()[0][3], running_pathname)) config_name = '/fwpm_config.ini' config_path = '' if os.path.exists(pwd.getpwuid(os.getuid())[5] + '/Library/Preferences' + config_name): config_path = pwd.getpwuid(os.getuid())[5] + '/Library/Preferences' + config_name else: if ".app/Contents" in running_pathname: running_root = '/'.join(running_pathname.split('/')[0:-3]) self.logger.info("%s: Application root folder: %s" % (inspect.stack()[0][3], running_root)) if os.path.exists(running_root + config_name): config_path = running_root + config_name else: if os.path.exists(running_pathname + config_name): config_path = running_pathname + config_name if not config_path: return self.logger.info("Configuration file: %s" % config_path) if not os.access(config_path, os.R_OK): self.logger.critical("Unable to access config file, check privileges.") return config = ConfigParser.SafeConfigParser(allow_no_value=True) config.read(config_path) self.config_options["flags"] = {} self.config_options["keyfile"] = {} self.config_options["logging"] = {} self.config_options["slack"] = {} for section in ["flags", "keyfile", "logging", "slack"]: for item in config.options(section): if item.startswith("use_"): # if "use_" in item: try: self.config_options[section][item] = config.getboolean(section, item) except Exception as exception_message: self.config_options[section][item] = False self.logger.error("%s: Invalid/Blank value: %s:%s. [%s]" % (inspect.stack()[0][3], section, item, exception_message)) elif "path" in item: self.config_options[section][item] = config.get(section, item) else: self.config_options[section][item] = config.get(section, item) self.logger.info("Configuration file variables:") for key, value in self.config_options.items(): self.logger.info(key) for sub_key, sub_value in value.items(): self.logger.info("\t%s %r" % (sub_key, sub_value)) def change_state(self): """ Handles toggling of FWPW """ self.logger.info("%s: activated" % inspect.stack()[0][3]) current_password = '' known_current_password = False new_fw_tool_path = '/usr/sbin/firmwarepasswd' new_fw_tool_exists = os.path.exists(new_fw_tool_path) if not new_fw_tool_exists: self.logger.critical("firmwarepasswd tool not found.") full_keylist = self.previous_keys full_keylist.append(self.current_key) if self.fwpw_status == 'On': self.status_string.set('Attempting to find current password...') new_fw_tool_cmd = [new_fw_tool_path, '-verify'] self.logger.info(' '.join(new_fw_tool_cmd)) for index in reversed(xrange(len(full_keylist))): try: child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -verify']) exit_condition = False while not exit_condition: result = child.expect(['Password:', 'password:', 'Correct', 'Incorrect', pexpect.EOF, pexpect.TIMEOUT]) if result == 0: child.sendline(self.admin_password) elif result == 1: child.sendline(full_keylist[index]) elif result == 2: current_password = full_keylist[index] known_current_password = True self.status_string.set('local password found.') self.logger.info('local password found.') break elif result == 3: # self.logger.info('#3.') break elif result == 4: # self.logger.info('#4.') break elif result == 5: # self.logger.info('#5.') break else: self.logger.error("%s: Unknown error. Exiting." % (inspect.stack()[0][3])) return if known_current_password: child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -delete']) result = child.expect('Password:') if result == 0: child.sendline(self.admin_password) result = child.expect('password:') if result == 0: child.sendline(current_password) result = child.expect(['NOTE', 'ERROR']) if result == 0: self.logger.info('Off.') elif result == 1: self.logger.info('PW incorrect.') else: self.logger.info('Error turning off.') try: child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/nvram -d fwpw-hash']) result = child.expect('Password:') if result == 0: child.sendline(self.admin_password) result = child.expect('') if result == 0: self.logger.info('removed nvram.') elif result == 1: self.logger.info('nvrmam oops 1') else: self.logger.info('nvram oops 2') else: pass except Exception as exception_message: self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) self.lock_label['image'] = self.double_exclamation_icon_photoimage self.state_string.set('FW password removed, reboot!') self.slack_message("_*" + self.local_identifier + "*_ :unlock:\n" + "FWPW and nvram entry removed.", '', 'info') break except Exception as exception_message: self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) else: # self.fwpw_status == 'Off' # ~/Box Sync/working stuff @ box/FWPM/skeleton key 4:48pm root@t-mcdaniel-mac-laptop #170 ]firmwarepasswd -setpasswd # Setting Firmware Password # Enter password: # Enter new password: # Re-enter new password: # ERROR | setPasswdFromCommandLine | Unable to verify password # ERROR | main | Exiting with error: 4 self.logger.info("Setting FW password") self.logger.info("Using %s" % self.current_key) if not self.current_key: self.logger.error('Blank key.') return child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -setpasswd']) result = child.expect('Password:') if result == 0: child.sendline(self.admin_password) result = child.expect('password:') if result == 0: child.sendline(self.current_key) else: pass result = child.expect('new password:') if result == 0: child.sendline(self.current_key) result = child.expect(['NOTE', 'ERROR']) if result == 0: self.logger.info('On.') elif result == 1: self.logger.info('PW incorrect.') else: self.logger.info('Error turning off.') try: child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/nvram fwpw-hash=2:' + self.hashed_results.get()]) result = child.expect('Password:') if result == 0: child.sendline(self.admin_password) result = child.expect('') if result == 0: self.logger.info('added nvram.') elif result == 1: self.logger.info('nvrmam oops 3') else: self.logger.info('nvram oops 4') else: pass except Exception as exception_message: self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) self.status_string.set('Password activated. Reboot!') self.state_string.set('FW password activated, reboot!') self.lock_label['image'] = self.shield_icon_photoimage self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW and hash updated.", '', 'info') def flush_keys(self): """ Erase loaded keys, reset UI. """ # "secure" erase keys # update label # deactivate button(s) # change icon self.previous_keys = [] self.current_key = '' self.keys_label['image'] = self.balloon_icon_photoimage self.keys_loaded_string.set('No keys in memory') self.hashed_results.set('') self.state_button_state = 'disabled' self.hash_button_state = 'disabled' self.hash_btn.configure(state='disabled') self.change_state_btn.configure(state='disabled') def slack_message(self, message, icon, msg_type): """ Sends slack messages. """ if self.logger: self.logger.info("%s: activated" % inspect.stack()[0][3]) slack_info_channel = False slack_error_channel = False if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_info_url"]: slack_info_channel = True if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_error_url"]: slack_error_channel = True if slack_error_channel and msg_type == 'error': slack_url = self.config_options["slack"]["slack_error_url"] elif slack_info_channel: slack_url = self.config_options["slack"]["slack_info_url"] else: return payload = {'text': message, 'username': 'Skeleton Key ' + self.master_version, 'icon_emoji': ':old_key:'} response = requests.post(slack_url, data=json.dumps(payload), headers={'Content-Type': 'application/json'}) self.logger.info('Response: ' + str(response.text)) self.logger.info('Response code: ' + str(response.status_code)) def slack_optionator(self): """ Builds the local identifier string per configuration file. ip, mac, hostname computername serial """ if self.logger: self.logger.info("%s: activated" % inspect.stack()[0][3]) if self.verify_network(): try: full_ioreg = subprocess.check_output(['ioreg', '-l']).decode('utf-8') serial_number_raw = re.findall('\"IOPlatformSerialNumber\" = \"(.*)\"', full_ioreg) serial_number = serial_number_raw[0] if self.config_options["slack"]["slack_identifier"].lower() == 'ip' or self.config_options["slack"]["slack_identifier"].lower() == 'mac' or self.config_options["slack"]["slack_identifier"].lower() == 'hostname': processed_device_list = [] # Get ordered list of network devices base_network_list = subprocess.check_output(["/usr/sbin/networksetup", "-listnetworkserviceorder"]).decode('utf-8') network_device_list = re.findall(r'\) (.*)\n\(.*Device: (.*)\)', base_network_list) ether_up_list = subprocess.check_output(["/sbin/ifconfig", "-au", "ether"]).decode('utf-8') for device in network_device_list: device_name = device[0] port_name = device[1] try: if port_name in ether_up_list: device_info_raw = subprocess.check_output(["/sbin/ifconfig", port_name]).decode('utf-8') mac_address = re.findall('ether (.*) \n', device_info_raw) ether_address = re.findall('inet (.*) netmask', device_info_raw) # if len(ether_address) and len(mac_address): if ether_address and mac_address: processed_device_list.append([device_name, port_name, ether_address[0], mac_address[0]]) except Exception as this_exception: self.logger.error("error discovering device info. [%s]" % this_exception) if processed_device_list: if self.logger: self.logger.info("1 or more active IP addresses. Choosing primary.") if self.config_options["slack"]["slack_identifier"].lower() == 'ip': self.local_identifier = processed_device_list[0][2] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" elif self.config_options["slack"]["slack_identifier"].lower() == 'mac': self.local_identifier = processed_device_list[0][3] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" elif self.config_options["slack"]["slack_identifier"].lower() == 'hostname': try: self.local_identifier = socket.getfqdn() except Exception as exception_message: if self.logger: self.logger.error("error discovering hostname. [%s]" % exception_message) self.local_identifier = serial_number else: if self.logger: self.logger.error("error discovering IP info.") self.local_identifier = serial_number elif self.config_options["slack"]["slack_identifier"].lower() == 'computername': try: cname_identifier_raw = subprocess.check_output(['/usr/sbin/scutil', '--get', 'ComputerName']) self.local_identifier = cname_identifier_raw.split('\n')[0] if self.logger: self.logger.info("Computername: %r" % self.local_identifier) except Exception as exception_message: if self.logger: self.logger.info("error discovering computername. [%s]" % exception_message) self.local_identifier = serial_number elif self.config_options["slack"]["slack_identifier"].lower() == 'serial': self.local_identifier = serial_number if self.logger: self.logger.info("Serial number: %r" % self.local_identifier) else: if self.logger: self.logger.info("bad or no identifier flag, defaulting to serial number.") self.local_identifier = serial_number except Exception as this_exception: self.logger.error("error verifying network. [%s]" % exception_message) self.config_options["slack"]["use_slack"] = False else: self.config_options["slack"]["use_slack"] = False if self.logger: self.logger.info("No network detected.") def verify_network(self): """ Verifies network availability. Host: 8.8.8.8 (google-public-dns-a.google.com) OpenPort: 53/tcp Service: domain (DNS/TCP) """ try: _ = requests.get("https://dns.google.com", timeout=3) # _ = requests.get("https://8.8.8.8", timeout=3) return True except requests.ConnectionError as exception_message: self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) return False def login(root, logger): """ aquire admin password """ logger.info("%s: activated" % inspect.stack()[0][3]) try: root.withdraw() if platform.system() == 'Darwin': tmpl = 'tell application "System Events" to set frontmost of every process whose unix id is {} to true' script = tmpl.format(os.getpid()) _ = subprocess.check_call(['/usr/bin/osascript', '-e', script]) password = tkSimpleDialog.askstring("Password", "Enter admin password:", show='*', parent=root) if not password: logger.error("%s: Canceled login." % (inspect.stack()[0][3])) return cmd_output = [] try: child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -check']) exit_condition = False while not exit_condition: result = child.expect(['WARNING:', '\n\nPass', 'Password:', 'attempts', pexpect.EOF, pexpect.TIMEOUT]) cmd_output.append(child.before) cmd_output.append(child.after) if result == 0: continue elif result == 1: child.sendline(password) elif result == 2: child.sendline(password) elif result == 3: logger.error("%s: Incorrect admin password." % (inspect.stack()[0][3])) sys.exit() elif result == 4: exit_condition = True elif result == 5: exit_condition = True else: logger.error("%s: Unknown error. Exiting." % (inspect.stack()[0][3])) return except Exception as exception_message: logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) # # begin parsing out useful content checked_output = [] for value in cmd_output: if isinstance(value, basestring): if "System\r\nAdministrator." in value: pass elif "WARNING" in value: pass elif "Improper" in value: pass elif value == '\r\n': pass elif not value: pass elif value == "Password:": pass else: checked_output.append(value) for item in checked_output: if 'Enabled' in item: if 'Yes' in item: logger.info("Yes. %r" % item) return password, 'On' else: logger.info("No. %r" % item) return password, 'Off' else: sys.exit() except ValueError: logger.error("%s: Error here." % (inspect.stack()[0][3])) return sys.exit() def main(): """ Entry into script. """ master_version = "1.0" logging.basicConfig(filename='/tmp/skeleton_key_v' + master_version + '.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) logger.info("Running Skeleton Key " + master_version) root = Tk() try: admin_password, fwpw_status = login(root, logger) except Exception as exception_message: logger.error("%s: Error logging in. [%s]" % (inspect.stack()[0][3], exception_message)) sys.exit(0) root.deiconify() SinglePane(root, logger, admin_password, fwpw_status, master_version) root.mainloop() if __name__ == '__main__': main()